﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using PServiceBus.Core.Interface;
using PServiceBus.Core.Security;
using PServiceBus.Core.Manager;
using System.Collections.Concurrent;
using System.Threading;
using PServiceBus.Core.Runtime.Gateway;
using PServiceBus.Core.Runtime.Configuration;
using PServiceBus.Services.Gateway.Configurations;
using PServiceBus.Core.Runtime;
using PServiceBus.Core.Runtime.Extensions;
using System.IO;
using System.Net;
using Phoenix.ZeroMQConnectors;
using System.Runtime.InteropServices;
using PServiceBus.Gateway.Interfaces;
using PServiceBus.Services.Gateway.Extensions;
using PServiceBus.Gateway.ZeroMQ;

namespace PServiceBus.Services.Gateway.Runtime {
    /// <summary>
    /// This class is used to connect to the ESB endpoint
    /// </summary>
    public static class ESB {
        private static string _address;
        internal static bool _reThrowException;
        private static IMessageBus _messageBus;
        private static volatile bool _serializationLoaded;
        private static volatile bool _disconnected;
        internal static HashSet<string> FailedEndpoints { get; set; }
        private static List<Action> _onDisconnects = new List<Action>();
        private static bool _autoBatch = false;

        static void Configure() {
            if (_serializationLoaded) return;
            PServiceBus.Services.Gateway.Runtime.SerializationBootstrapper.Configure();
            PServiceBus.Transports.SerializationBootstrapper.Configure();
            PServiceBus.Core.Runtime.SerializationBootstrapper.Configure();
            _serializationLoaded = true;
        }

        static ESB() {
            Headers = new Dictionary<string, string>();
            FailedEndpoints = new HashSet<string>();
            AppDomain.CurrentDomain.ProcessExit += Shutdown;
            AppDomain.CurrentDomain.DomainUnload += Shutdown;
            AppDomain.CurrentDomain.ProcessExit += Shutdown;
            _consoleHandler += new EventHandler(Handler);
            SetConsoleCtrlHandler(_consoleHandler, true);

            var routerConfig = ConfigurationFactory.GetConfigSection<ESBRouterConfigSection>();
            if (routerConfig != null) {
                var routers = routerConfig.Routers;
                foreach (ESBRouterElement router in routers)
                    ESBGateway.AddRouterAddress(router.Endpoint);
            }
            Connect();
        }

        [DllImport("Kernel32")]
        private static extern bool SetConsoleCtrlHandler(EventHandler handler, bool add);

        private delegate bool EventHandler(CtrlType sig);
        private static EventHandler _consoleHandler;

        private enum CtrlType {
            CTRL_C_EVENT = 0,
            CTRL_BREAK_EVENT = 1,
            CTRL_CLOSE_EVENT = 2,
            CTRL_LOGOFF_EVENT = 5,
            CTRL_SHUTDOWN_EVENT = 6
        }

        private static bool Handler(CtrlType sig) {
            switch (sig) {
                case CtrlType.CTRL_C_EVENT:
                case CtrlType.CTRL_LOGOFF_EVENT:
                case CtrlType.CTRL_SHUTDOWN_EVENT:
                case CtrlType.CTRL_CLOSE_EVENT:
                    ESB.Disconnect();
                    return false;
                default:
                    return false;
            }
        }

        internal static void Disconnect() {
            if (_disconnected) return;
            _disconnected = true;
            for (var i = 0; i < _onDisconnects.Count; i++)
                _onDisconnects[i]();
            if (_messageBus != null && !_messageBus.IsClosed) _messageBus.Dispose();
            _onDisconnects.Clear();
        }

        internal static void Shutdown(object sender, EventArgs e) {
            Disconnect();
        }

        /// <summary>
        /// Return true if connection to esb endpoint is available
        /// </summary>
        public static bool IsConnected {
            get {
                return !MessageBus.IsClosed;
            }
        }

        /// <summary>
        /// Register delegate to call on disconnection from current esb endpoint
        /// </summary>
        public static Action OnDisconnect {
            set {
                _onDisconnects.Add(value);
            }
        }

        internal static bool AutoBatch {
            get { return _autoBatch; }
        }

        internal static IMessageBus MessageBus {
            get { 
                return _messageBus ?? (_messageBus = CreateMessageBus()); 
            }
        }

        internal static bool ThrowException { get { return _reThrowException; } }

        public static Dictionary<string, string> Headers { private get; set; }

        /// <summary>
        /// Enable auto-batching of similar message into a single message to improve throughput
        /// Messages can be lost if process publishing message shutdown before message is pushed to ESB
        /// Batching can cause a delay of up to 100ms
        /// When enable always delay processing shutdown by at least 100ms
        /// </summary>
        /// <param name="autoBatch"></param>
        public static void EnableAutoBatch(bool autoBatch) {
            _autoBatch = autoBatch;
        }

        private static void CallReconnectCallback() {
            if (ESBGateway.ReconnectCallback != null) ESBGateway.ReconnectCallback();
        }

        internal static void Execute(Action<IMessageBus> action) {
            var messageBus = MessageBus;
            try {
                action(messageBus);
            } catch (Exception ex) {
                if (!GatewayMessageBus.IsValidException(ex)) return;
                ESBGateway.LastEndpoint.IsAvailable = false;
                CallReconnectCallback();
                Execute(action);
            }
        }

        private static IMessageBus CreateMessageBus() {
            IMessageBus messageBus = null;
            if (_address != null)
                messageBus = GatewayMessageBus.Create(_address, ESBPrincipal.Current.ESBIdentity, _reThrowException, Headers);
            else {
                messageBus = new MessageBus() { ReThrowException = _reThrowException, Headers = Headers };
                if (messageBus.Identity == null) messageBus.Identity = new ESBIdentity(Environment.UserName, string.Empty, true);
            }
            return messageBus;
        }

        /// <summary>
        /// Get list of registered topics
        /// </summary>
        /// <returns></returns>
        public static IList<Topic> GetTopics() {
            return MessageBus.GetTopics().Select(x => x.ToTopic()).ToList();
        }
        
        /// <summary>
        /// Enable throwing of exception when using the ESB API
        /// </summary>
        /// <param name="reThrowException">Will throw all exception if set to true</param>
        public static void ReThrowException(bool reThrowException) {
            _reThrowException = reThrowException;
        }
        

        /// <summary>
        /// Configure ESB Connection endpoint
        /// </summary>
        /// <param name="address"></param>
        [Obsolete("Call ESB.Connect instead. Call is not required if connecting to localhost")]
        public static void ConfigWithAddress(string address) {
            Configure();
            _address = address;
            ESBGateway.AddRouterAddress(address);
        }

        /// <summary>
        /// Connect to ESB at the specified endpoint
        /// </summary>
        /// <param name="host"></param>
        /// <param name="port"></param>
        public static void Connect(string host = "localhost", int port = 8000) {
            Configure();
            var ipAddress = Dns.GetHostAddresses(host)
                .First(x => x.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork)
                .ToString();
            _address = String.Format("endpoint://{0}:{1}", ipAddress, port);
            ESBGateway.AddRouterAddress(_address);
        }

        /// <summary>
        /// Set Authentication Identity
        /// </summary>
        /// <param name="username"></param>
        /// <param name="password"></param>
        public static void Authenticate(string username, string password) {
            ESBPrincipal.Current = new ESBPrincipal(new ESBIdentity(username, password));
        }

        /// <summary>
        /// Create an ESBTransport to send a one way message
        /// </summary>
        /// <typeparam name="TTransport"></typeparam>
        /// <param name="configAction"></param>
        /// <returns></returns>
        public static ESBTransport With<TTransport>(Action<TTransport> configAction) where TTransport : ITransport {
            return new ESBTransport(Transport.New<TTransport>(configAction));
        }
    }
}
